CVE-2019-3560 整数溢出导致Facebook Fizz拒绝服务
原文链接:https://lgtm.com/blog/facebook_fizz_CVE-2019-3560
这篇文章是关于我在Facebook的Fizz项目中使用Semmle QL查询发现的拒绝服务漏洞。
该漏洞是一个无限循环,可以由未经身份验证的远程攻击者触发。Fizz是Facebook的TLS实现,这意味着它用于
https://facebook.com
的“https:”
部分。在一篇关于Fizz的博客文章中,Facebook工程师Kyle Nekritz,Subodh Iyengar和Alex Guzman在2018-08-06发表了一篇关于Fizz,并且这篇文章描述了在Facebook上展开的内容:
我们已经在我们的移动应用程序Proxygen,我们的负载平衡器,内部服务甚至我们的QUIC库mvfst中全局部署了Fizz和TLS 1.3。现在,超过50%的互联网流量使用TLS 1.3进行保护。
Fizz是一个开源项目,因此很可能其他项目和组织也在使用它。
漏洞影响
此漏洞的影响是攻击者可以通过TCP向任何使用Fizz的服务器发送恶意消息并在该服务器上触发无限循环。这可能会使服务器无法响应其他客户端。
该漏洞被归类为拒绝服务(DoS攻击),因为它使攻击者能够破坏服务,但不会获得未经授权的访问。消息的大小刚刚超过64KB,
因此这种攻击对于攻击者来说非常便宜,但对服务器来说却是瘫痪的。为了说明这一点,一台具有普通国内级互联网连接(1Mbps上传速度)的
计算机每秒可以发送两条这样的消息。由于每条消息都会敲出一个CPU内核,因此只需要一个小型僵尸网络即可快速削弱整个数据中心。
Facebook很快修复了漏洞。我在2019-02-20向他们报告了,他们在2019-02-25将修复程序推送到了GitHub。Facebook已经通知我,
他们在2019-02-20之间的几小时内修补了他们自己的所有服务器。
除了升级Fizz之外,我不知道有任何针对此漏洞的缓解措施,因此我建议所有Fizz用户尽快升级。该漏洞已在版本v2019.02.25.00中修复。
漏洞利用
我写了一个触发漏洞的概念验证。它是一个简单的C程序,它向服务器打开一个TCP套接字,并发送一个大小超过64KB的恶意负载。
程序在发送有效负载后立即关闭套接字,但服务器没有注意到这一点,因为它已经陷入无限循环。我没有测试任何真实网站上的有效负载,
只测试了Fizz源代码中包含的演示服务器应用程序。但是,该漏洞是Fizz库的核心,而不是演示应用程序,因此我认为https://facebook.com在我报告此漏洞之前存在风险。
Facebook已经修补了他们的系统并且不再容易受到攻击,但我会在发布漏洞利用PoC的源代码之前等待几周,以便其他Fizz用户有时间升级。
漏洞分析
该漏洞是由PlaintextRecordLayer.cpp的+=第42行中的整数溢出引起的:
1. auto length = cursor.readBE<uint16_t>(); 1. if (buf.chainLength() < (cursor - buf.front()) + length) { 1. return folly::none; 1. } 1. length += 1. sizeof(ContentType) + sizeof(ProtocolVersion) + sizeof(uint16_t); 1. buf.trimStart(length); 1. continue;
此代码uint16_t从传入的网络数据包中读取a并将其分配给length。换句话说,length是攻击者控制的。在if所在的第39行的语句
看起来有点像边界检查,但它实际上只是检查是否已收到足够的数据以继续解析。这就是漏洞利用需要发送64KB数据的原因:代码不会在第42行上
遇到整数溢出,直到它至少接收到length字节为止。该漏洞利用设置length = 0xFFFB
。这意味着在之后+=
,值为length0
.
这反过来意味着trimStart
对第43行的调用不消耗任何数据,因此在循环的下一次迭代之前不会进行任何进展。
对于漏洞修复:使用比uint16_t
计算加法更大的类型,因此不可能出现整数溢出。
我还没有详细说明漏洞利用的工作原理。设置length = 0xFFFB
很容易!我发现如何构建一条实际触发这行代码的消息会有点棘手。为了让其他Fizz用户有时间升级,我将等待几周才能发布漏洞利用的全部细节。
漏洞发现
在他们的博客文章中,Facebook的工程师将Fizz描述为“从头开始安全”,并列出了他们用来避免常见陷阱(如错误的状态机转换)
的一些C ++编程技术。我同意Fizz的一般代码质量看起来非常好。它使用现代C ++风格,因此不太可能遭受困扰旧C项目的一些错误。特别是,
它不进行任何手动内存管理,因此它不太可能遭受缓冲区溢出等问题,这在其他项目中很常见。Facebook也告诉我他们使用模糊测试关于Fizz,
他们已经与外部咨询公司进行了安全审查。换句话说,这是一个高质量的项目,团队正在按照最佳实践做所有事情。那么这个错误是如何通过网络传播的?QL是如何找到它的?
当你看到PlaintextRecordLayer.cpp的第42行,很明显它包含整数溢出。当然,困难在于知道要查看哪行代码。
模糊测试对于自动查找错误非常有效,但它基于随机生成的输入,因此当偶然命中某些代码路径的概率非常低时,它不能很好地工作。
(正如我上面提到的,编写漏洞利用程序最难的部分是弄清楚如何构建一个能够到达易受攻击的代码行的输入)但是QL并没有以同样的方式受到限制。
使用QL,我可以搜索任何潜在的整数溢出,无论它们触发的难度如何。有些结果在实践中可能无法触发,但最好是安全而不是遗憾。但是,我不能指望开发人员修复数以千计的“错误”
我最初发现此漏洞的查询略有不同,但我的同事Jonas Jensen提出了我将在这里使用的改进版本。它比我原来的查询更准确,
它还展示了我们新的C ++中间表示,这是目前正在开发的一个新功能。当有多个源语法用于相同操作时,IR有助于简化查询。例如,以下三行代码都完全相同:
1. x = x+1; 1. x += 1; 1. x++;
如果没有IR,在代码中查找添加内容的查询将需要针对每种语法的单独子句。但是使用IR,查询只需要一个子句AddInstruction。
使用IR,让我们首先编写一个查询,查找从较大类型到较小类型的所有转换。这些转换可能会溢出。
1. import cpp 1. import semmle.code.cpp.ir.IR 1. 1. from ConvertInstruction conv 1. where conv.getResultSize() < conv.getUnary().getResultSize() 1. select conv
如果您想尝试自己运行此查询,那么您只需要下载QL for Eclipse和Fizz快照。可在此处找到有关如何使用QL for Eclipse的说明。
或者,您可以在LGTM的查询控制台中运行查询。使用查询控制台的缺点是它只允许您查询最新版本的源代码。因此,由于Facebook已经修复了漏洞,
您将无法使用查询控制台找到它。正如您所料,上面的查询在代码中发现了许多缩小的转换。这是找到潜在溢出的有用起点,
但我们需要缩小结果数量。如果可以由攻击者故意触发整数溢出,则它只是一个安全漏洞。因此,我们需要使用有关哪些转换可能依赖于
不受信任的输入值的上下文信息来增强查询。Jonas的查询通过使用新的IR污染跟踪库来实现这一点查找依赖于不受信任的输入的表达式。
但不信任的输入来自哪里?这通常取决于应用程序,因此通常有助于构建应用程序攻击面的模型。在Fizz的情况下,事实证明不受信任的输入
通过另一个名为Folly的 Facebook库到达。Folly将数据放入IOBuf,然后由Fizz读取。因此,模拟不受信任数据来源的一种方法IOBuf是在Fizz中找到s的所有用途。
但是我们发现了一个不同的解决方案,它对Fizz项目既简单又不太具体:当通过套接字发送数据时,它通常以网络字节顺序发送。
因此,网络数据通常需要转换为主机字节顺序,通常使用ntohs或ntohl。这意味着,ntohs和ntohl往往是“不可信的输入”优秀代理。
唯一的障碍是Fizz不使用ntohs和ntohl!相反,它使用Endian该类。以下QL类标识其方法Endian用于将网络字节顺序转换为主机:
1. class EndianConvert extends Function { 1. EndianConvert() { 1. (this.getName() = "big") and 1. this.getDeclaringType().getName().matches("Endian") 1. } 1. }
总而言之,这是Jonas的查询,它使用污点跟踪来查找可能依赖于不受信任的输入的表达式的不安全缩小转换:
1. /** 1. * @name Fizz Overflow 1. * @description Narrowing conversions on untrusted data could enable 1. * an attacker to trigger an integer overflow. 1. * @kind path-problem 1. * @problem.severity warning 1. */ 1. 1. import cpp 1. import semmle.code.cpp.ir.dataflow.TaintTracking 1. import semmle.code.cpp.ir.IR 1. import DataFlow::PathGraph 1. 1. /** 1. * The endianness conversion function `Endian::big()`. 1. * It is Folly's replacement for `ntohs` and `ntohl`. 1. */ 1. class EndianConvert extends Function { 1. EndianConvert() { 1. this.getName() = "big" and 1. this.getDeclaringType().getName().matches("Endian") 1. } 1. } 1. 1. class Cfg extends TaintTracking::Configuration { 1. Cfg() { this = "FizzOverflowIR" } 1. 1. /** Holds if `source` is a call to `Endian::big()`. */ 1. override predicate isSource(DataFlow::Node source) { 1. source.(CallInstruction).getCallTarget().(FunctionInstruction).getFunctionSymbol() instanceof 1. EndianConvert 1. } 1. 1. /** Hold if `sink` is a narrowing conversion. */ 1. override predicate isSink(DataFlow::Node sink) { 1. sink.getResultSize() < sink.(ConvertInstruction).getUnary().getResultSize() 1. } 1. } 1. 1. from 1. Cfg cfg, DataFlow::PathNode source, DataFlow::PathNode sink, ConvertInstruction conv, 1. Type inputType, Type outputType 1. where 1. cfg.hasFlowPath(source, sink) and 1. conv = sink.getNode() and 1. inputType = conv.getUnary().getResultType() and 1. outputType = conv.getResultType() 1. select sink, source, sink, 1. "Conversion of untrusted data from " + inputType + " to " + outputType + "."
此查询只有一个结果,即上述漏洞。对包含修复的代码的较新修订版本的查询结果为零。
Bug Bounty在2019-03-13,我从Facebook收到了这条好消息:
嗨Kevin Backhouse,
在审核此问题后,我们决定奖励您10000美元的奖金。以下是赏金金额的解释。Facebook通过Bugcrowd实现了赏金奖励。
此漏洞可能允许恶意用户对Facebook基础结构进行拒绝服务。
虽然拒绝服务问题通常不被视为我们的错误赏金计划的一部分,但此提交文件讨论了可能存在重大风险的情况。
再次感谢您的报告。我们期待将来收到更多您的报告!
在Semmle团队,我们的政策是向慈善机构捐赠所有的奖金,本次所得的奖励也不会例外,将全部捐助慈善机构。
时间线
1. 2019-02-20:私下透露给Facebook的白帽计划。 1. 2019-02-20:报告由Facebook承认并转发给他们的产品团队。 1. 2019-02-20:Facebook修补了他们所有的服务器。 1. 2019-02-25:Facebook将修复程序推向了[GitHub](https://github.com/facebookincubator/fizz/commit/40bbb161e72fb609608d53b9d64c56bb961a6ee2)。 1. 2019-03-13:Facebook确认Bug赏金。 1. 2019-03-19:Semmle公布的[CVE- 2019-3560](http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-3560)。